/* 
 * Licensed Materials - Property of IBM
 * (c) Copyright IBM Corporation 2010. All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:  Use,
 * duplication or disclosure restricted by GSA ADP Schedule
 * Contract with IBM Corp.
 *
 * This code is released under the terms of the Eclipse Public License
 * version 1.0 which can be found at the top level of this project or
 * or at http://www.eclipse.org/org/documents/epl-v10.php
 */

package com.ibm.mining;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.ibm.mining.model.*;

public class PathQueryTranslator {

	Model model;
	
	public static void main(String args[]) {
		Model model = new JazzMiningModel();
		PathQueryTranslator pqt = new PathQueryTranslator(model);
		for (String pathQuery : args) {
			System.out.println(pqt.translatePathQuery(pathQuery));
		}
		if (args.length == 0) {
			test();
		}
	}

	/* test a number of things */
	public static void test() {
		Model model = new JazzMiningModel();
		PathQueryTranslator pqt = new PathQueryTranslator(model);
		pqt.translatePathQuery("select c.email from WorkItem->creator:c, WorkItem->resolver:r");
		pqt.translatePathQuery("select * from WorkItem->target->development_line");
		/* select w.id, a.email_address from workitem:w->approvals->approver:a */
		pqt.translatePathQuery("select a.name from WorkItem  -> approvals -> approver   :  a  ");
		pqt.translatePathQuery("select t.tag from WorkItem:w->resolver:r, Workitem:w2->creator:c where w.id > 100 and foo>bar " + 
				" and a is not null or 4 < 6 and w = w2 and r = c group by foobar");
		pqt.translatePathQuery("select r.name, c.name, w.id, w.summary from workitem:w->creator:c, workitem:w2->resolver:r" +
				"where r = c and w = w2");
		
		pqt.translatePathQuery("select c.uuid, v.path from changeset:c->changes:ch->after_state:v" +
				" where (ch.kind = 'ADD' or ch.kind = 'MODIFY')");
		
		pqt.translatePathQuery("select w.id, a.path from Workitem:w, link l, changeset:ch->changes->after_state:a" +
				"where l.source_item_id = ch.uuid and l.target_item_id = w.uuid order by w.id, a.path");

		pqt.translatePathQuery("select ch.uuid, a.path from changeset:ch->changes->after_state:a");
		
		pqt.translatePathQuery("Select a.email, count(*) from workitem:w->approvals->approver:a" +
				" where w.status = 'Resolved' group by a.email");
	
		// who has made comments on workitems scheduled to be finished in the foo iteration
		// that has made changes to component bar
		pqt.translatePathQuery("select a.name, comm.content, w1.summary from workitem:w1->comments:comm->creator:c, workitem:w2->target:t, " +
				"changeset:ch1->author:a, changeset:ch2->component:comp where c.uuid = a.uuid " +
				"and comp.name = 'UI' and t.name='Milestone 2' and w1 = w2 and ch1 = ch2");
		
		pqt.translatePathQuery("select w.id, comp.name, a.path from workitem:w, link as l, " +
				"changeset:cs->component:comp, changeset:cs2->changes->after_state:a " +
				"where cs = cs2 and l.target_item_id = w.uuid and l.source_item_id = cs.uuid " +
				"and l.link_type = 'com.ibm.team.filesystem.workitems.change_set'");
		
		pqt.translatePathQuery("select comp.name, a.path, w.id from  " +
				"changeset:cs->changes->after_state:a, changeset:cs2->component:comp, " +
				"workitem as w, link as l " +
				"where cs = cs2 and l.target_item_id = w.uuid " +
				"and l.source_item_id = cs.uuid " +
				"");
	}
	
	public PathQueryTranslator(Model model) {
		this.model = model;
	}

	public PathQueryTranslator() {
		this.model = new JazzMiningModel();
	}
	
	class PathElement {
		Property property;
		Type type;
		ModelClass modelClass;
		String relationName;
		PathElement previous;
		
		
		PathElement(String relationName, Type type, Property property, PathElement previous) {
			this.property = property;
			this.type = type;
			if (type.isModelClassType() || 
					type.isListItemType() && type.getListItemType().isModelClassType()) {
				this.modelClass = type.getBaseModelClassForType();
			}
			this.relationName = relationName;
			this.previous = previous;
		}
		
		public String toString() {
			return "PathElement : <" + ((property == null) ? "NO_PROPERTY" : property.getName())
				+ ", " + type.getName() +
				", " + relationName + ">";
		}
	}
	
	
	List<String> joins;
	List<String> clauses;
	public String translatePathQuery(String pathQuery) {
		System.out.println("Path Query : " + pathQuery + "\n");
		autoRelationsMap = new HashMap<String, Integer>();
		relationToTableMap = new HashMap<String, String>();
		pathElementMap = new HashMap<String, PathElement>();
		joins = new LinkedList<String>();
		clauses = new LinkedList<String>();

		/* parse out the query into four parts.  This is pretty much a disaster.
		 * This should have been possible with a regular expression, but I couldn't
		 * get non-greedy matching to work, so here we are.
		 */
		String prefix = "";
		String fromPart = "";
		String wherePart = "";
		String suffix;
		Matcher fromMatcher = Pattern.compile("from", Pattern.CASE_INSENSITIVE).matcher(pathQuery);
		fromMatcher.find();
		prefix = pathQuery.substring(0, fromMatcher.end());
		Matcher whereMatcher = Pattern.compile("where", Pattern.CASE_INSENSITIVE).matcher(pathQuery);
		if (whereMatcher.find()) {
			fromPart = pathQuery.substring(fromMatcher.end(), whereMatcher.start() );
			Matcher suffixMatcher = Pattern.compile("(group|order|having).*").matcher(pathQuery);
			if (suffixMatcher.find()) {
				wherePart = pathQuery.substring(whereMatcher.end(), suffixMatcher.start());
				suffix = suffixMatcher.group();
			} else {
				wherePart = pathQuery.substring(whereMatcher.end());
				suffix = "";
			}
		} else {
			wherePart = "";
			Matcher suffixMatcher = Pattern.compile("(group|order|having).*", 
					Pattern.CASE_INSENSITIVE).matcher(pathQuery);
			if (suffixMatcher.find()) {
				fromPart = pathQuery.substring(fromMatcher.end(), suffixMatcher.start());
				suffix = suffixMatcher.group();
			} else {
				fromPart = pathQuery.substring(fromMatcher.end());
				suffix = "";
			}
			
		}
		
		System.out.println("prefix is : " + prefix);
		System.out.println("From part is : " + fromPart);
		System.out.println("Where part is : " + wherePart);
		System.out.println("suffix is : " + suffix);
		
		List<String> froms = new LinkedList<String>();
		for (String path : fromPart.split(",")) {
			/* if there is a ":" or a "->" then it's a path.
			 * otherwise, just include the source verbatim
			 */
			if (path.contains(":") || path.contains("->")) {
				processPath(path);
			} else {
				froms.add(path);
			}
			
		}
		
		String transformedClauses = "";
		if (wherePart.length() > 1) {
			/* this splitter keeps the delimiters so we can tell if 
			 * it is an and or an or
			 */
			Splitter splitter = new Splitter(Pattern.compile("and|or", Pattern.CASE_INSENSITIVE));
			String parts[] = splitter.split(wherePart);
			for (int i = 0; i < parts.length; i++) {
				String newClause = processClause(parts[i]);
				i += 1;
				transformedClauses += newClause + " " + ((i < parts.length) ? (parts[i] + " ") : "");
			}
		}
		
		for (String key : relationToTableMap.keySet()) {
			froms.add(relationToTableMap.get(key) + " as " + key);
		}
		String query = prefix;
		query += "\n" + Util.join(froms, ", \n");
		query += "\nwhere \n" + Util.join(joins, " and \n");
		if (transformedClauses.length() > 0) {
			query += "\n and " + transformedClauses + " \n";
		}
		if (suffix != null) {
			query += "\n" + suffix;
		}
		
		System.out.println("Query : \n" + query + "\n\n");
		
		return query;
	}
	
	String processClause(String clause) {
		String newClause = "";
		
		Matcher m = Pattern.compile("(\\s*[()]*\\s*)(\\w+)\\s*(!=|=)\\s*(\\w+)(\\s*[()]*\\s*)")
			.matcher(clause);
		if (m.matches()) {
			/* first we need to do a lot of checking to make sure that
			 * this clause is type-correct, 'cause it's likely that someone
			 * will make a mistake and they would like to know where it was 
			 * made
			 */
			String leftName = m.group(2);
			String rightName = m.group(4);
			String operator = m.group(3);
			/* check that both names are real relations */
			MyAssert.check(pathElementMap.containsKey(leftName),
				"Relation " + leftName + " in clause " + clause + " is not defined");
			MyAssert.check(pathElementMap.containsKey(rightName),
				"Relation " + rightName + " in clause " + clause + " is not defined");
			PathElement leftElement = pathElementMap.get(leftName);
			PathElement rightElement = pathElementMap.get(rightName);
			Type leftType = leftElement.type;
			if (leftType.isListItemType()) leftType = leftType.getListItemType();
			Type rightType = rightElement.type;
			if (rightType.isListItemType()) rightType = rightType.getListItemType();
			/* check that these two relations are of the correct type */
			MyAssert.check(leftType == rightType, 
					"Cannot compare " + leftElement.type + " to " + rightElement.type
					+ " in where clause " + clause);
			if (leftElement.type.isModelClassType()) {
				newClause += "(";
				boolean first = true;
				for (String key : leftElement.modelClass.getKeys()) {
					if (first) {
						first = false;
					} else {
						newClause += " and ";
					}
					newClause += leftName + "." + key + operator + rightName + "." + key;
				}
				newClause += ")";
				return newClause;
			}
		} else {
			return "(" + clause + ")";
		}
		
		return newClause;
	}
	
	void processPath(String path) {
		List<PathElement> pathElements =
			parsePath(path);
		
		for (int i=0; i < pathElements.size()-1; i++) {
			PathElement from = pathElements.get(i);
			PathElement to = pathElements.get(i+1);
			/* if there is a 1 to 1 or many to 1 relation, then
			 * we just do a straight join
			 */
			if (to.property.getType().isModelClassType()) {
				addJoin(from.relationName, to.property.getName(), 
						to.relationName, "", to.modelClass.getKeys());
			} else if (to.property.getType().isListItemType()) {
				/* we need to join across the from_to_to relation
				 */
				/* if to is a list of primitives, then the table name to join on
				 * is to.relationName
				 */
				if (to.type.getListItemType().isPrimitiveType()) {
					addJoin(from.relationName, "",
							to.relationName, from.modelClass.getName(),
							from.modelClass.getKeys());
					
				/* if the property is a list of ModelClassType then
				 * we join the joining relation with another relation 
				 * otherwise, the joining relation is all we want since 
				 * it holds the needed data.
				 */
				} else if (to.type.getListItemType().isModelClassType()) {
					String joiningTableName = from.type.getModelClassForType().getName() + "_to_" +
						to.property.getName();
					String joiningRelation = getRelationName(null, joiningTableName);
					addJoin(from.relationName, "",
							joiningRelation, from.modelClass.getName(),
							from.modelClass.getKeys());
					String joiningColumnBase = Util.getSingular(to.property.getName()) + "_" +
						to.modelClass.getName();
					addJoin(joiningRelation, joiningColumnBase,
							to.relationName, "",
							to.modelClass.getKeys());
				} else {
					throw new RuntimeException("Bad list type " + to.type);
				}
			}
		}
	}
	
	void addJoin(String fromRelation, String fromColBase, 
			String toRelation, String toColBase, 
			List<String> keys) {
		for (String key : keys) {
			String fromColumn = (fromColBase == null || fromColBase.length() == 0) ? 
					key : fromColBase + "_" + key;
			String toColumn = (toColBase == null || toColBase.length() == 0) ? 
					key : toColBase + "_" + key;
			String join = fromRelation + "." + fromColumn + " = " + toRelation + "." + toColumn;
			joins.add(join);
		}
	}
	
	/* parse a single path and return a list of pairs of (model class, relation name)
	 * pairs which represent the class
	 */
	Map<String, PathElement> pathElementMap;
	public List<PathElement> parsePath(String path) {
		List<PathElement> relations = 
			new LinkedList<PathElement>();
	
		//System.out.println("parsing : " + path);
		ModelClass mc = null; 
		PathElement lastRelation = null, relation = null;
		Pattern p = Pattern.compile("^\\s*(->|<-)?\\s*(\\w+)\\s*(:\\s*(\\w+)\\s*)?");
		for (Matcher m = p.matcher(path); m.lookingAt(); m.region(m.end(), m.regionEnd())) {
			//System.out.println("dir : |" + m.group(1) + "| base : |" + m.group(2) + "| name : |" +
			//		m.group(4) + "|");
			/* the name of the modelclass or property */
			String name = m.group(2);
			/* if the user specified the name of the relation, as in Workitem:w */
			String specifiedRelationName = m.group(4);
			/* is this a forward or backward link (the first element has no direction) */
			boolean forward = (m.group(1) != null && m.group(1).equals("->"));
			
			Type type;
			String element = null;
			String relationName = null;
			if (lastRelation == null) {
				/* this must be the first element in the path */
				mc = model.getModelClass(name);
				type = model.getType(mc.getName());
				relationName = getRelationName(specifiedRelationName, mc.getName());
				relation = new PathElement(relationName, type, null, null);
			} else {
				/* we're after the first element in the path so grab the
				 * "foo" from Bar->foo:f and get the property from Bar
				 */
				Property prop = lastRelation.modelClass.getProperty(name);
				/* now get that property's type */
				type = prop.getType();
				
				if (type.isPrimitiveType()) {
					String err = "Cannot include property " + prop.getName() + " of " +
						lastRelation.modelClass.getName() +
						"\n because it is a primitive type property";
					throw new RuntimeException(err);
				} else if (type.isModelClassType()) {
					relationName = getRelationName(specifiedRelationName, type.getName());
					relation = new PathElement(relationName, type, prop, lastRelation);
				} else if (type.isListItemType() &&
						type.getListItemType().isPrimitiveType()) {
					String tableName = lastRelation.modelClass.getName() + "_to_" + prop.getName();
					relationName = getRelationName(specifiedRelationName, tableName);
					relation = new PathElement(relationName, type, prop, lastRelation);
					/* the type is a list of primitive type, like Workitem.Tags */
				} else if (type.isListItemType() &&
						type.getListItemType().isModelClassType()) {
					/* the type is a list of Model Class types, like Workitem.Approvals */
					relationName = getRelationName(specifiedRelationName, type.getListItemType().getName());
					relation = new PathElement(relationName, type, prop, lastRelation);
					
				}
			}
			//System.out.println("Adding relation : " + relation);
			lastRelation = relation;
			pathElementMap.put(relationName, relation);
			relations.add(relation);
		}
		
		return relations;
	}

	/* this map keeps track of how many times each tablename has been used
	 * so we make sure there are no conflicts so it generates workitem_1, workitem_2, etc.
	 */
	Map<String, Integer> autoRelationsMap;
	/* this keeps the mapping of relation names to table names
	 * the key is the relation name as used in the select statement
	 * and the value is the name of the table.
	 */
	Map<String, String> relationToTableMap;
	/* we need to come up with the name of the relation in the query. 
	 * if the path had a specified Relation name then use it
	 * otherwise create one based on the table that it comes from by appending
	 * _index and incrementing index
	 */
	String getRelationName(String specifiedRelationName, String tableName) {
		if (specifiedRelationName != null) {
			MyAssert.check(!relationToTableMap.containsKey(specifiedRelationName), 
					"Cannot use the name " + specifiedRelationName + " more than once");
			autoRelationsMap.put(specifiedRelationName, 1);
			relationToTableMap.put(specifiedRelationName, tableName);
			return specifiedRelationName;
		} else {
			int index = 1;
			if (autoRelationsMap.containsKey(tableName)){
				index = autoRelationsMap.get(tableName) + 1;
			}
			autoRelationsMap.put(tableName, index);
			String relationName = tableName + "_" + index;
			relationToTableMap.put(relationName, tableName);
			return relationName;
		}
	}
}
